/*
 *
 * Copyright (C) 2012 Walter Lütgenau
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 * 
 *      http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 * 
 */
package offset.nodes.client.veditor.model;

import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.net.URL;
import java.net.URLDecoder;
import java.net.URLEncoder;
import java.util.HashMap;
import javax.swing.text.AttributeSet;
import javax.swing.text.BadLocationException;
import javax.swing.text.Element;
import javax.swing.text.SimpleAttributeSet;
import javax.swing.text.html.HTML;
import javax.swing.text.html.HTML.Attribute;
import javax.swing.text.html.HTML.Tag;
import offset.nodes.Constants;
import offset.nodes.client.model.HttpUtils;
import offset.nodes.client.model.ServerModel;
import offset.nodes.client.editor.Editor;
import offset.nodes.client.editor.model.DocumentContext;
import offset.nodes.client.editor.model.HyperlinkModel;
import offset.nodes.client.editor.model.Range;
import offset.nodes.client.editor.model.messages.GetWorkflowTransitions;
import offset.nodes.client.editor.view.hyperlink.DataHyperlinkInfo;
import offset.nodes.client.editor.view.hyperlink.ExternalHyperlinkInfo;
import offset.nodes.client.editor.view.hyperlink.InternalHyperlinkInfo;
import offset.nodes.client.veditor.view.ev.NodeFromTypeChooser;
import offset.nodes.client.veditor.view.ev.NodeToTypeChooser;

/**
 *
 * @author Walter Luetgenau
 */
public class VirtualHyperlinkModel extends HyperlinkModel {

    public static final String NODE_REFERENCE = Constants.PAR_NODE_REFERENCE;
    public static final String DATA_REFERENCE = Constants.PAR_DATA_REFERENCE;
    public static final String TEMPLATE_REFERENCE = Constants.PAR_TEMPLATE_INSTANCE_REFERENCE;
    public static final String VIEW_SELECT_NODE_FROM_THIS = NodeFromTypeChooser.class.getName(); 
    public static final String VIEW_SELECT_NODE_TO_THIS = NodeToTypeChooser.class.getName(); 
    public static final String REPOSITORY_URL =
            Constants.PATH_SEPARATOR + Constants.CONTEXTMAPPING_NODES + Constants.PATH_SEPARATOR + Constants.SERVLETMAPPING_REPOSITORY;
    
    DataModel dataModel = null;

    public VirtualHyperlinkModel(DocumentContext context, Editor editor) {
        super(context, editor);
        this.dataModel = new DataModel(context, editor.getProperties());
    }

    class WorkflowProperties {

        boolean workflow;
        String[] transitions;

        public WorkflowProperties(boolean workflow) {
            this.workflow = workflow;
        }

        public String[] getTransitions() {
            return transitions;
        }

        public void setTransitions(String[] transitions) {
            this.transitions = transitions;
        }

        public boolean isWorkflow() {
            return workflow;
        }

        public void setWorkflow(boolean workflow) {
            this.workflow = workflow;
        }
    }

    /**
     * From the download URL, determine the UUID of the document. If it is not present (i.e. a path is present), this is no workflow document.
     * Request the currently possible transitions for the workflow document from the server. Store the transitions in the editor properties.
     *
     * @return the workflow properties, i.e. the transitions
     * @throws Exception
     */
    protected WorkflowProperties getWorkflowProperties() throws Exception {
        WorkflowProperties workflowProperties = (WorkflowProperties) getEditor().getProperties().get(Editor.PROP_WORKFLOW_PROPERTIES);
        if (workflowProperties == null) {
            String templatePath = (String) getEditor().getProperties().get(Editor.PROP_TEMPLATE_NODE_PATH);
 
            String serverUrl = (String) getEditor().getProperties().get(Editor.PROP_UPLOAD_TO);
            ServerModel serverModel = new ServerModel(new URL(serverUrl));

            GetWorkflowTransitions.Request request = new GetWorkflowTransitions.Request();
            request.setTemplateNodePath(templatePath);

            GetWorkflowTransitions.Response response = (GetWorkflowTransitions.Response) serverModel.sendRequest(request);
            if (response.getTransitions() == null)
                workflowProperties = new WorkflowProperties(false);
            else {
                workflowProperties = new WorkflowProperties(true);
                workflowProperties.setTransitions(response.getTransitions());
            }

            getEditor().getProperties().put(Editor.PROP_WORKFLOW_PROPERTIES, workflowProperties);
        }

        return workflowProperties;
     }

    /**
     * Is this a workflow document?
     *
     * @return
     * @throws Exception
     */
    public boolean isWorkflowDocument() throws Exception {
        return getWorkflowProperties().isWorkflow();
    }

    /**
     * Retrieve the transitions possible in the current state of the workflow document. If the editor document is not a
     * workflow document, return null;
     *
     * @return the possible transitions, if this is a workflow document, else null
     * @throws Exception
     */
    public String[] getWorkflowTransitions() throws Exception {
        if (!isWorkflowDocument())
            return null;

        return getWorkflowProperties().getTransitions();
    }

    /**
     * Insert a data hyperlink. 
     * 
     * If the node shall be selected by the user, the other fields except the hyperlink will not be editable, use n-edit attribute to achieve this. 
     * In addition, the secondary query root node shall have n-edit=explicit set. 
     * If the reference is fromThis, the root node must as well store temporarily the uuid of the selected node as attribute n-primary-reference.
     * 
     * @param range The text, that shall be substituted by the hyperlink
     * @param parameters The hyperlink parameters
     * @param data The data attribute
     * @throws java.lang.Exception
     */
    public void insertDataHyperlink(Range range, DataHyperlinkInfo parameters, String data) throws Exception {
        Element e = getRealParagraph(range.getStartOffset());
        if (getRealParagraph(range.getEndOffset()) != e)
            return; // can only insert as a span into a paragraph element

        String reference = "";
        String viewType = "";
        if (parameters.getContentType().equals(Constants.CONTENT_TYPE_VIRTUAL) 
                && parameters.getVirtualPageTemplateUUID() != null
                && parameters.getVirtualPageTemplateUUID().length() > 0) {
            reference = "&" + TEMPLATE_REFERENCE + "=" + parameters.getVirtualPageTemplateUUID();
            viewType = "&" + Constants.PAR_CONTENT_TYPE + "=" + Constants.CONTENT_TYPE_VIRTUAL;
        }
        else if (parameters.getContentType().equals(Constants.CONTENT_TYPE_WORKFLOW)) {
            if (parameters.getWorkflowTransition() != null && parameters.getWorkflowTransition().length() > 0)
                reference = "&" + Constants.PAR_TRANSITION + "=" + URLEncoder.encode(parameters.getWorkflowTransition(), "UTF-8") + "&";
            viewType = "&" + Constants.PAR_CONTENT_TYPE + "=" + Constants.CONTENT_TYPE_WORKFLOW;
        }

        String hyperlinkAttributes = Attribute.HREF.toString() + "=\"" + REPOSITORY_URL + "?" +
                DATA_REFERENCE + "={../@jcr:uuid}" +
                reference +
                viewType + "\" " +
                DataModel.ATTRIBUTE_NAME_DATA + "=\"" + data + "\" " +
                (parameters.isSelectNode() ? DataModel.ATTRIBUTE_NAME_EDIT + "=\"" + DataModel.ATTRIBUTE_VALUE_TRUE + "\" " +
                (parameters.getReferenceType() == DataHyperlinkInfo.REFERENCE_TO_THIS ? DataModel.ATTRIBUTE_NAME_VIEW + "=\"" + VIEW_SELECT_NODE_TO_THIS + "\" " : DataModel.ATTRIBUTE_NAME_VIEW + "=\"" + VIEW_SELECT_NODE_FROM_THIS + "\" ")
                : "") ;

        String hyperlink = "<" + Tag.A.toString() + " " +
                hyperlinkAttributes + ">" +
                getDocument().getText(range.getStartOffset(), range.getEndOffset() - range.getStartOffset()) +
                "</" + Tag.A.toString() + ">";

        String html = getStartTag(e) +
                getDocument().getText(e.getStartOffset(), range.getStartOffset() - e.getStartOffset()) +
                hyperlink +
                getDocument().getText(range.getEndOffset(), e.getEndOffset() - range.getEndOffset()) +
                getEndTag(e);

        if (parameters.isSelectNode()) {
            StringBuffer buf = new StringBuffer();

            Element root = findSecondaryRoot(e);
            SimpleAttributeSet atts = new SimpleAttributeSet(root.getAttributes());
            if (parameters.getReferenceType() == DataHyperlinkInfo.REFERENCE_FROM_THIS)
                atts.addAttribute(DataModel.ATTRIBUTE_NAME_PRIMARY_REFERENCE, "{@jcr:uuid}");
            buf.append(getStartTag(atts, DataModel.ATTRIBUTE_NAME_EDIT + "=\"" + DataModel.ATTRIBUTE_VALUE_EXPLICIT + "\""));
            buf.append(addInnerHtmlAndSubstitute(root, e, html));
            buf.append(getEndTag(root));

            html = buf.toString();
            e = root;
        }

        setOuterHTML(e, html);
    }

    protected String addInnerHtmlAndSubstitute(Element root, Element substitute, String substituteText) throws BadLocationException, IOException {
        StringBuffer buf = new StringBuffer();

        for (int i = 0; i < root.getElementCount(); i++) {
            if (root.getElement(i) == substitute)
                buf.append(substituteText);
            else
                buf.append(getOuterHTML(root.getElement(i)));
        }

        return buf.toString();
    }

    protected Element findSecondaryRoot(Element e) {
        while (e != null && !e.getAttributes().isDefined(DataModel.ATTRIBUTE_NAME_SCHEMA)) {
            e = e.getParentElement();
        }

        if (e.getAttributes().isDefined(DataModel.ATTRIBUTE_NAME_SCHEMA) && ((String) e.getAttributes().getAttribute(DataModel.ATTRIBUTE_NAME_DATA)).startsWith(DataModel.SECONDARY_QUERY))
            return e;

        return null;
    }

    /**
     * Insert a data hyperlink directly referencing the uuid of the target data node.
     * 
     * @param range The text, that shall be substituted by the hyperlink
     * @param parameters The hyperlink parameters
     * @param data The data attribute
     * @throws java.lang.Exception
     */
    public void insertDirectDataHyperlink(Range range, DataHyperlinkInfo parameters) throws Exception {
        Element e = getRealParagraph(range.getStartOffset());
        if (getRealParagraph(range.getEndOffset()) != e)
            return; // can only insert as a span into a paragraph element

        String hyperlink = "<" + Tag.A.toString() + " " +
                Attribute.HREF.toString() + "=\"" + REPOSITORY_URL + "?" +
                DATA_REFERENCE + "=" + parameters.getTargetUuid() + 
                (parameters.getVirtualPageTemplateUUID() != null ? "&" +TEMPLATE_REFERENCE + "=" + parameters.getVirtualPageTemplateUUID() : "") +
                "&contentType=virtual\">" +
                getDocument().getText(range.getStartOffset(), range.getEndOffset() - range.getStartOffset()) +
                "</" + Tag.A.toString() + ">";

        String html = getStartTag(e) +
                getDocument().getText(e.getStartOffset(), range.getStartOffset() - e.getStartOffset()) +
                hyperlink +
                getDocument().getText(range.getEndOffset(), e.getEndOffset() - range.getEndOffset()) +
                getEndTag(e);

        setOuterHTML(e, html);

    }

    /**
     * The "HREF" attribute of the hyperlink element will be subdivided into several parameters. Return a hash map of these parameters.
     * 
     * @param parameterString The HREF string
     * @return a hash map with the parsed parameters
     */
    protected HashMap<String, String> getParameters(String parameterString) {
        HashMap<String, String> result = new HashMap<String, String>();

        int start = parameterString.indexOf("?");
        if (start < 0)
            return result;

        parameterString = parameterString.substring(start + 1);

        String[] parameters = parameterString.split("&");
        for (int i = 0; i < parameters.length; i++) {
            String[] pair = parameters[i].split("=");
            if (pair.length < 2)
                continue;
            result.put(pair[0], pair[1]);
        }

        return result;
    }

    /**
     * Return the parent node type
     * 
     * @param e The element, whose parent node type shall be determined
     * @return the parent node type.
     */
    public String getParentNodeType(Element e) {
        return dataModel.getParentNodeType(e);
    }

    /**
     * Extract hyperlink information from the hyperlink element.
     * 
     * @param e The element
     * @return the hyperlink information
     */
    public DataHyperlinkInfo getDataHyperlinkInfo(Element e) throws UnsupportedEncodingException {
        AttributeSet hyperlinkAttributes = (AttributeSet) e.getAttributes().getAttribute(HTML.Tag.A);
        if (hyperlinkAttributes == null) // if no hyperlink at all
            return null;

        String href = (String) hyperlinkAttributes.getAttribute(HTML.Attribute.HREF);
        if (href == null) // something wrong here
            return null;

        if (href.indexOf(HyperlinkModel.DATA_REFERENCE) < 0) // not a data hyperlink
            return null;

        HashMap<String, String> parameters = getParameters(href);

        DataHyperlinkInfo result = new DataHyperlinkInfo();

        if (parameters.get(Constants.PAR_CONTENT_TYPE) != null)
            result.setContentType(parameters.get(Constants.PAR_CONTENT_TYPE));
        if (parameters.get(TEMPLATE_REFERENCE) != null)
            result.setVirtualPageTemplateUUID(parameters.get(TEMPLATE_REFERENCE));
        else if (parameters.get(Constants.PAR_TRANSITION) != null)
            result.setWorkflowTransition(URLDecoder.decode(parameters.get(Constants.PAR_TRANSITION), "UTF-8"));
        String nameProperty = (String) hyperlinkAttributes.getAttribute(DataModel.ATTRIBUTE_NAME_DATA);
        if (nameProperty != null) {
            int index = nameProperty.lastIndexOf("/");
            if (index > 0)
                nameProperty = nameProperty.substring(index + 1);
            if (nameProperty.startsWith("@"))
                nameProperty = nameProperty.substring(1);
        }
        result.setNameProperty(nameProperty);
        String nodeTypeName = getParentNodeType(e);
        result.setNodeTypePrefixName(nodeTypeName);

        if (hyperlinkAttributes.isDefined(DataModel.ATTRIBUTE_NAME_VIEW)) {
            String viewer = (String) hyperlinkAttributes.getAttribute(DataModel.ATTRIBUTE_NAME_VIEW);
            if (viewer.equals(VIEW_SELECT_NODE_FROM_THIS))
                result.setReferenceType(DataHyperlinkInfo.REFERENCE_FROM_THIS);
            else
                result.setReferenceType(DataHyperlinkInfo.REFERENCE_TO_THIS);
            result.setSelectNode(true);
        } else {
            Element secondaryRoot = findSecondaryRoot(e);
            if (secondaryRoot == null)
                result.setReferenceType(DataHyperlinkInfo.REFERENCE_SUBNODE);
            else
                result.setReferenceType(DataHyperlinkInfo.REFERENCE_TO_THIS);
            result.setSelectNode(false);
        }

        return result;
    }

    /**
     * Insert a hyperlink referring to a node. The hyperlink will refer to the UUID, if it is present, else to the path of the node.
     * 
     * @param range The range within the document, that shall be substituted by the hyperlink
     * @param info information about the hyperlink
     * @throws java.lang.Exception
     */
    public void insertInternalHyperlink(Range range, InternalHyperlinkInfo info) throws Exception {
        Element e = getRealParagraph(range.getStartOffset());
        if (getRealParagraph(range.getEndOffset()) != e)
            return; // can only insert as a span into a paragraph element

        // workaround for swing bug with p-implied paragraphs
        e = getParagraphWithNoImpliedSiblings(e);

        String hyperlink = null;
        String hrefValue = "";

        if (info.getReference() != null)
            hrefValue = REPOSITORY_URL + "?" +
                    NODE_REFERENCE + "=" + info.getReference();
        else
            hrefValue = REPOSITORY_URL + info.getPath();


        if (info.getValueProperty() != null && info.getValueProperty().length() > 0)
            hrefValue = HttpUtils.addParameter(hrefValue, Constants.PAR_PROPERTY_NAME, info.getValueProperty());

        if (info.getReference() != null)
            hyperlink = "<" + Tag.A.toString() + " " 
                    +  Attribute.HREF.toString() + "=\"" + hrefValue
                    +  "\">" + info.getValue() +
                    "</" + Tag.A.toString() + ">";
        else
            hyperlink = "<" + Tag.A.toString() + " " +
                    Attribute.HREF.toString() + "=\"" + REPOSITORY_URL + info.getPath() +
                    "\">" + info.getValue() +
                    "</" + Tag.A.toString() + ">";

        String html = substituteRange(e, hyperlink, range.getStartOffset(), range.getEndOffset());

        setOuterHTML(e, html);
    }

    /**
     * Insert a hyperlink to the internet.
     * 
     * @param range The range, that shall be substituted by the hyperlink
     * @param info information about the hyperlink
     */
    public void insertExternalHyperlink(Range range, ExternalHyperlinkInfo info) throws Exception {
        Element e = getRealParagraph(range.getStartOffset());
        if (getRealParagraph(range.getEndOffset()) != e)
            return; // can only insert as a span into a paragraph element

        String hyperlink = "<" + Tag.A.toString() + " " +
                Attribute.HREF.toString() + "=\"" + info.getUrl() +
                "\">" + info.getText() +
                "</" + Tag.A.toString() + ">";

        String html = substituteRange (e, hyperlink, range.getStartOffset(), range.getEndOffset());
        setOuterHTML(e, html);
    }
}
